iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
自我挑戰組

程式碼門診:診斷壞味道、開出重構處方系列 第 6

讓函式只做一件事 - 單一功能原則 (Single Responsibility Principle)

  • 分享至 

  • xImage
  •  

簡單介紹

在軟體開發當中幾乎都會聽過的 SOLID 原則,其源自於 Robert C. Martin,原文如下

"A class should have only one reason to change"

翻譯成中文為「一個類別應該只有一個改變的理由」,今天我們就來提及關於單一職責原則,在程式當中的意思簡單的說法就是:每個函式、類別或模組都應該只負責一件事,並且只有一個改變的理由。

換句話說,當我們需要修改程式碼時,每次的修改應該只影響單一功能。這個原則能讓我們的程式碼更容易理解、測試和維護。

我們用生活中的例子來舉例,例如主廚專注煮菜、掌控味道與擺盤,服務生則負責端菜、與客人互動並結帳;如果同一個人既要煮又要跑桌,可能會忙成一團、出菜變慢、顧客等待時間拉長,也很難針對某一流程做優化。

對應到程式設計,就是不要把使用者介面、金額計算、輸入驗證與 API 呼叫全塞在同一個元件或函式裡——應該把「計算金額」「驗證輸入」「呼叫 API」「顯示 UI」各自拆開負責,讓每個部分都能專注且容易維護。

下面用同一個業務邏輯示範兩個版本

不好的範例

這個範例違反了單一職責原則,因為它將 UI 呈現、業務邏輯、資料驗證、資料存取以及資料格式化等多種職責全部混在同一個元件裡。當需求變動時,例如稅率調整、驗證規則更新或 API 格式更改,都會迫使這個我們需要找尋需要修改的位置,當業務邏輯上升,以許未來資料驗證邏輯更多、需要格式化以及存取的部分更多,則要找尋程式碼的地方就像大海撈針一樣,導致維護成本提高、測試困難度增加,也讓程式碼的可讀性與可重用性大幅下降。

import React, { useState } from "react";

export const OrderCheckoutBad: React.FC = () => {
  const [quantity, setQuantity] = useState(1);
  const [price, setPrice] = useState(100);
  const [email, setEmail] = useState("");
  const [loading, setLoading] = useState(false);
  
  const handleSubmit = async () => {
    if (quantity <= 0 || price <= 0) { alert("數量/單價需大於 0"); return; }
    if (!/^\S+@\S+$/.test(email)) { alert("Email 格式錯誤"); return; }
    const subtotal = quantity * price;
    const tax = Math.round(subtotal * 0.05);
    const total = subtotal + tax;
    const currency = new Intl.NumberFormat("zh-TW",{style:"currency",currency:"TWD"}).format(total);
    try {
      setLoading(true);
      const response = await fetch("/api/checkout",{
        method:"POST",
        headers:{"Content-Type":"application/json"},
        body:JSON.stringify({ qty: quantity, price, email, total })
      });
      if (!response.ok) throw new Error("network");
      alert(`下單成功,金額:${currency}`);
    } catch { 
      alert("下單失敗,請稍後再試"); 
    } finally { 
      setLoading(false); 
    }
  };
  
  return (
    <div>
      <h3>Checkout (Bad)</h3>
      <input 
        type="number" 
        value={quantity} 
        onChange={(event) => setQuantity(+event.target.value)} 
      />{" "}
      <input 
        type="number" 
        value={price} 
        onChange={(event) => setPrice(+event.target.value)} 
      />{" "}
      <input 
        placeholder="email" 
        value={email} 
        onChange={(event) => setEmail(event.target.value)} 
      />{" "}
      <button disabled={loading} onClick={handleSubmit}>
        {loading ? "處理中…" : "送出訂單"}
      </button>
      <p>預估總計:{quantity * price + Math.round(quantity * price * 0.05)}</p>
    </div>
  );
};

好的範例

下面這個範例就符合 SRP 原則,因為它將不同的職責清楚拆分:金額計算由 calculateTotal 負責,驗證規則集中在 validateOrder,金額格式化交給 formatTWD,與 API 溝通的邏輯則獨立在 checkoutApi,而 React 元件本身只專注於狀態管理與 UI 呈現。這種分工方式讓每個模組都有單一變動原因,例如修改稅率只需調整 TAX_RATE 與計算函式、變更驗證邏輯只需改 validateOrder,不會影響其他部分,讓程式碼更易讀、易測試。

type Order = { qty: number; price: number; email: string; };
const TAX_RATE = 0.05;

const calculateTotal = ({ qty, price }: Order) => { 
  const subtotal = qty * price; 
  const tax = Math.round(subtotal * TAX_RATE); 
  return { subtotal, tax, total: subtotal + tax }; 
};

const validateOrder = ({ qty, price, email }: Order) => 
  qty <= 0 ? "數量需大於 0" : 
  price <= 0 ? "單價需大於 0" : 
  !/^\S+@\S+$/.test(email) ? "Email 格式錯誤" : 
  null;

const formatTWD = (amount: number) => 
  new Intl.NumberFormat("zh-TW", {style:"currency", currency:"TWD"}).format(amount);

const checkoutApi = async (orderData: Order & { total: number }) => 
  Promise.resolve({ ok: true });

export const OrderCheckoutGood: React.FC = () => {
  const [form, setForm] = useState<Order>({ qty: 1, price: 100, email: "" });
  const [loading, setLoading] = useState(false);
  const totals = calculateTotal(form);
  
  const handleSubmit = async () => {
    const error = validateOrder(form); 
    if (error) { 
      alert(error); 
      return; 
    }
    setLoading(true); 
    const response = await checkoutApi({ ...form, total: totals.total }); 
    setLoading(false);
    alert(response.ok ? `下單成功,金額:${formatTWD(totals.total)}` : "下單失敗");
  };
  
  return (
    <div>
      <h3>Checkout (Good)</h3>
      <input 
        type="number" 
        value={form.qty} 
        onChange={(event) => setForm({ ...form, qty: +event.target.value })} 
      />{" "}
      <input 
        type="number" 
        value={form.price} 
        onChange={(event) => setForm({ ...form, price: +event.target.value })} 
      />{" "}
      <input 
        placeholder="email" 
        value={form.email} 
        onChange={(event) => setForm({ ...form, email: event.target.value })} 
      />{" "}
      <button disabled={loading} onClick={handleSubmit}>
        {loading ? "處理中…" : "送出訂單"}
      </button>
      <p>預估總計:{formatTWD(totals.total)}</p>
    </div>
  );
};

總結與參考來源

綜合以上所述,我們成功透過單一功能原則讓程式碼變得更加模組化和可維護。記住,好的程式碼應該讓每個函式都有明確的單一目的。當我們在寫程式時,可以時常問自己:「這個函式是否只做一件事?」

參考資料


上一篇
YAGNI 原則 - You Aren't Gonna Need It!
下一篇
對擴展開放,對修改封閉 - 開放封閉原則 (Open-Closed Principle)
系列文
程式碼門診:診斷壞味道、開出重構處方7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言